package pk.contender.earmouse;
import android.content.Context;
import android.util.JsonReader;
import android.util.JsonWriter;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
/**
* Abstraction of a set of exercises that belong together, e.g. all basic major intervals.
* <p>
* Manages a module's data and state, generates exercises and has several I/O methods
* for reading and writing Module objects.
* @author Paul Klinkenberg <pklinken.development@gmail.com>
*/
public class Module implements Comparable<Module>{
/** Constants referring to the difficulty of a Module */
final static public int DIFF_BEGINNER = 1, DIFF_AMATEUR = 2, DIFF_INTERMEDIATE = 3, DIFF_EXPERT = 4;
private final Context mCtx;
/** The unique ID of this Module */
private int id;
/** The title of this Module */
private String title;
/** The description of this Module's contents or purpose */
private String description;
/** The lowest and highest notes this Module is allowed to use in its exercises (lowestNote, highestNote)
* In these variable, 0 refers to C2 and 41 to E5 */
private int lowestNote, highestNote;
/** The difficulty of this Module */
private int difficulty;
/** Reference to this Module's statistics */
private ModuleStats stats;
/** The version of create_module.py used to create this Module */
private String toolVersion;
/** Module version number
* <p>
* Not currently used for anything, planned use is for Module updates.
*/
private int moduleVersion = 1;
/** A short description on the Module contents
* <p>
* Used in ListViews to give the user an indication of a Module's contents beyond the title
*/
private String shortDescription;
/** List of the answers for this Module's exercises */
private List<String> answerList = new ArrayList<>();
/** List of this Module's Exercises */
private final List<Exercise> exerciseList = new ArrayList<>();
/**
* Contructs an empty (and useless) Module
* <p>Used when a fully initialised Module object is not required (e.g. ListViews)
*/
public Module (Context context) {
mCtx = context;
id = -1;
title = description = shortDescription = null;
lowestNote = highestNote = -1;
answerList = null;
difficulty = -1;
toolVersion = "n/a";
moduleVersion = 1;
}
/**
* Constructs a Module by reading a JSON from the given filename
* @param context The application context
* @param moduleFile The File from which to read the JSON data
*/
public Module (Context context, File moduleFile) {
mCtx = context;
try {
FileInputStream fis = new FileInputStream(moduleFile);
FileReader filereader = new FileReader(fis.getFD());
initModuleFromJson(filereader);
filereader.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
stats = new ModuleStats(mCtx, id);
}
/**
* Constructs a Module by reading a JSON from the given InputStreamReader
* @param context The application context
* @param reader The Reader from which to read the JSON data
*/
public Module(Context context, InputStreamReader reader) {
mCtx = context;
try {
initModuleFromJson(reader);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Save a Module's statistics
* @see ModuleStats#saveModuleStats()
*/
public void saveState () {
stats.saveModuleStats();
}
/**
* Reload this Module's statistics
*/
public void refreshState() {
stats = new ModuleStats(mCtx, id);
}
/**
* Loads this Module's properties and data from the JSON data of the given Reader
* @param r The Reader from which to read the JSON data
* @throws IOException
*/
private void initModuleFromJson(Reader r) throws IOException {
JsonReader reader = new JsonReader(r);
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "moduleId":
this.id = reader.nextInt();
break;
case "title":
this.title = reader.nextString();
break;
case "description":
this.description = reader.nextString();
break;
case "shortDescription":
this.shortDescription = reader.nextString();
break;
case "lowestNote":
this.lowestNote = reader.nextInt();
break;
case "highestNote":
this.highestNote = reader.nextInt();
break;
case "difficulty":
this.difficulty = reader.nextInt();
break;
case "version":
this.toolVersion = reader.nextString();
break;
case "moduleVersion":
this.moduleVersion = reader.nextInt();
break;
case "exerciseList":
reader.beginArray();
for (int i = 0; reader.hasNext(); i++) {
this.exerciseList.add(new Exercise());
reader.beginArray();
for (int j = 0; reader.hasNext(); j++) {
this.exerciseList.get(i).exerciseUnits.add(new ArrayList<Integer>());
reader.beginArray();
while (reader.hasNext()) {
this.exerciseList.get(i).exerciseUnits.get(j).add(reader.nextInt());
}
reader.endArray();
}
reader.endArray();
}
reader.endArray();
break;
case "answerList":
reader.beginArray();
while (reader.hasNext()) {
this.answerList.add(reader.nextString());
}
reader.endArray();
break;
default:
reader.skipValue();
break;
}
}
reader.endObject();
reader.close();
}
/**
* Generate a random number between [0 - limit>, with a linearly descending distribution from 0 to limit.
* <p>Example distribution of 10000 calls with limit == 5:
* Occurrences of 0: 3322
* Occurrences of 1: 2630
* Occurrences of 2: 2016
* Occurrences of 3: 1371
* Occurrences of 4: 661
*
* @param limit the upper limit of return values.
* @return a random number between [0 - limit> in a linearly descending distribution from 0 to limit.
*/
private int getLinearRandomNumber(int limit) {
Random rng = new Random();
int randomMultiplier = limit * (limit + 1) / 2;
int randomNumber = rng.nextInt(randomMultiplier);
int result = 0;
for(int i = limit; randomNumber >= 0; i--) {
randomNumber -= i;
result++;
}
return result - 1;
}
/**
* Returns an index to one of this Module's exercises that is random but weighted towards certain properties.
* <p>
* Specifically, it sorts all available exercises first on success rate and then on how often they were attempted.
* It then uses {@link #getLinearRandomNumber} to pick one, thus preferring items higher on the list.
*
* @return A weighted index to one of this Module's exercises.
*/
public int getWeightedExerciseIndex() {
/**
* Combines an exercise with the current success rate and the frequency of occurrences.
* Allows sorting exercises based on those factors.
* Implemented Comparable sorted on success rate first and if equal sorts on frequency of occurrence.
*/
//noinspection NullableProblems
class ratedExercise implements Comparable<ratedExercise> {
int exerciseIndex;
int successRate;
int exerciseCount;
ratedExercise(int exerciseIndex, int successRate, int exerciseCount) {
this.exerciseIndex = exerciseIndex;
this.successRate = successRate;
this.exerciseCount = exerciseCount;
}
int getSuccessRate() { return successRate; }
int getExerciseIndex() { return exerciseIndex; }
int getExerciseCount() { return exerciseCount; }
@Override
public int compareTo(ratedExercise another) {
if (this.getSuccessRate() == another.getSuccessRate())
return this.getExerciseCount() - another.getExerciseCount();
else
return this.getSuccessRate() - another.getSuccessRate();
}
}
List<ratedExercise> ratedExerciseList = new ArrayList<>();
for(int index = 0; index < exerciseList.size(); index++) {
ratedExerciseList.add(new ratedExercise(index, stats.exerciseSuccessRate(index), stats.exerciseCount(index)));
}
Collections.sort(ratedExerciseList);
// ratedExerciseList is now the list of all exercises in this module sorted by success rate and count
return ratedExerciseList.get(getLinearRandomNumber(ratedExerciseList.size())).getExerciseIndex();
}
/**
* @return The Module's title
*/
public String getTitle() {
return title;
}
/**
* @return The Module's description
*/
public String getDescription() {
return description;
}
/**
* @return The Module's difficulty
*/
public int getDifficulty() {
return difficulty;
}
/**
* @return The Module's answer list
*/
public List<String> getAnswerList() {
return answerList;
}
/**
* Generates an Exercise that can be used by {@link pk.contender.earmouse.MediaFragment} to generate
* a WAV sample.
* <p>
* The Exercise objects in {@link #exerciseList} are an abstract representation of a sequence of notes/chords.
* This function maps the Exercise at the given index to a random point between {@link #lowestNote} and {@link #highestNote}.
* The result can be used by {@link pk.contender.earmouse.MediaFragment} to generate a WAV sample.
* @param exerciseIndex The index of the Exercise to generate.
* @return An Exercise instance that can be used to prepare a WAV sample.
*/
public Exercise getExercise(int exerciseIndex) {
Exercise resultExercise = new Exercise();
int positiveOffset = 0;
int negativeOffset = 0;
for(List<Integer> exerciseUnit : exerciseList.get(exerciseIndex).exerciseUnits) {
if(exerciseUnit.get(0) < negativeOffset)
negativeOffset = exerciseUnit.get(0);
int span = 0;
for(int i = 1;i < exerciseUnit.size(); i++) {
if(exerciseUnit.get(i) > span)
span = exerciseUnit.get(i);
}
if((exerciseUnit.get(0) + span) > positiveOffset)
positiveOffset = (exerciseUnit.get(0) + span);
}
// At this point negativeOffset is the largest negative offset to be found in this exercise
// and positiveOffset is the largest positive offset.
// So now we can generate a random baseOffset that will not exceed the bounds of highestNote
// or lowestNote as prescribed by the Module.
Random rng = new Random();
int delta = (highestNote - positiveOffset) - (lowestNote - negativeOffset);
int baseOffset = rng.nextInt(delta) + (lowestNote - negativeOffset);
Log.d("Debug", "exerciseIndex: " + exerciseIndex + " positiveOffset:" + positiveOffset
+ " negativeOffset:" + negativeOffset + " delta: " + delta + "baseOffset: " + baseOffset);
// Now we can derive the values for the resultExercise using the baseOffset
for(int i = 0;i < exerciseList.get(exerciseIndex).exerciseUnits.size(); i++) {
resultExercise.exerciseUnits.add(new ArrayList<Integer>());
resultExercise.exerciseUnits.get(i).add(baseOffset + exerciseList.get(exerciseIndex).exerciseUnits.get(i).get(0));
for(int j = 1;j < exerciseList.get(exerciseIndex).exerciseUnits.get(i).size(); j++) {
resultExercise.exerciseUnits.get(i).add(resultExercise.exerciseUnits.get(i).get(0) + exerciseList.get(exerciseIndex).exerciseUnits.get(i).get(j));
}
}
return resultExercise;
}
/**
* Register an answer with the ModuleStats instance
* @param exerciseIndex Index of the Exercise we are registering an answer for
* @param result The correctness of the answer
*/
public void registerAnswer(int exerciseIndex, boolean result) {
stats.addAnswer(exerciseIndex, result);
}
/**
* @return The success rate for this Module
* @see ModuleStats#calculateSuccessRate()
*/
public int getSuccessRate() {
return stats.calculateSuccessRate();
}
/**
* @return the Module ID
*/
public int getId() {
return id;
}
/**
* Set the Module unique ID
* @param id The ID to set
*/
public void setId(int id) {
this.id = id;
}
/**
* Set the Module title
* @param title The title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* Set the Module difficulty
* @param difficulty The difficulty to set
*/
public void setDifficulty(int difficulty) {
this.difficulty = difficulty;
}
/**
* Attempts to write this Module to the device's local storage.
* <p>
* Write this Module to disk as a JSON file, does not save the value of ModuleStats
* @return True on success, false otherwise (this happens if an exception occurs)
*/
public boolean writeModuleToJson() {
File currentDir = mCtx.getDir("files", Context.MODE_PRIVATE);
File modFile = new File(currentDir, "module" +
Main.getLocaleSuffix() + "_" +
id + ".json");
if (!modFile.exists()) {
try {
//noinspection ResultOfMethodCallIgnored
modFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
FileWriter fw;
try {
fw = new FileWriter(modFile);
} catch (IOException e) {
e.printStackTrace();
return false;
}
try {
JsonWriter writer = new JsonWriter(fw);
writer.beginObject();
writer.name("moduleId");
writer.value(this.getId());
writer.name("title");
writer.value(this.getTitle());
writer.name("description");
writer.value(this.getDescription());
writer.name("shortDescription");
writer.value(this.getShortDescription());
writer.name("lowestNote");
writer.value(this.lowestNote);
writer.name("highestNote");
writer.value(this.highestNote);
writer.name("difficulty");
writer.value(this.getDifficulty());
writer.name("version");
writer.value(this.getToolVersion());
writer.name("moduleVersion");
writer.value(this.getModuleVersion());
writer.name("answerList");
writer.beginArray();
for(String answer : answerList) {
writer.value(answer);
}
writer.endArray();
writer.name("exerciseList");
writer.beginArray();
for(Exercise exercise : exerciseList) {
writer.beginArray();
for(List<Integer> exerciseUnit : exercise.exerciseUnits) {
writer.beginArray();
for(Integer value : exerciseUnit) {
writer.value(value);
}
writer.endArray();
}
writer.endArray();
}
writer.endArray();
writer.endObject();
writer.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
// Since we changed the local contents we should reload Main.mModules.
Main.refreshModuleList(mCtx);
return true;
}
/**
* Remove this Module and its associated ModuleStats file from local storage
* @return True if successful, false otherwise.
*/
@SuppressWarnings("UnusedReturnValue")
public boolean purgeModule() {
File currentDir = mCtx.getDir("files", Context.MODE_PRIVATE);
File modFile = new File(currentDir, "module" +
Main.getLocaleSuffix() + "_" +
id + ".json");
if(!stats.purgeStats())
Log.d("DEBUG", "stats.purgeStats() returned false");
return modFile.exists() && modFile.delete();
}
/**
* Reset the statistics for this Module
*/
public void resetStats() {
if(stats != null)
if(!stats.purgeStats())
Log.d("DEBUG", "Error deleting statistics");
stats = new ModuleStats(mCtx, id);
}
/**
* Comparable implementation, sorts first on difficulty, then on title.
* @see Comparable
*/
@Override
public int compareTo(@SuppressWarnings("NullableProblems") Module another) {
if(this.getDifficulty() == another.getDifficulty()) {
// Modules have same difficulty, subsort by title
return this.getTitle().compareToIgnoreCase(another.getTitle());
} else
return this.getDifficulty() - another.getDifficulty();
}
/**
* Returns the version number of the create_module.py tool that was used to create this module
* @return the version number of the create_module.py tool that was used to create this module
*/
String getToolVersion() {
return this.toolVersion;
}
/**
* @see ModuleStats#exercisesCompleted
*/
public int getExercisesCompleted() {
return stats.exercisesCompleted();
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) { this.shortDescription = shortDescription; }
int getModuleVersion() {
return moduleVersion;
}
public void setModuleVersion(int moduleVersion) {
this.moduleVersion = moduleVersion;
}
}